<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="g4Q8t" id="g4Q8t"><span data-lake-id="ua121ed4d" id="ua121ed4d">典型回答</span></h1>
  <p data-lake-id="u9672cdaf" id="u9672cdaf"><br></p>
  <p data-lake-id="ufb30231c" id="ufb30231c"><span data-lake-id="u8cb81e20" id="u8cb81e20">Dubbo是通过JDK的shutdown hook来完成优雅停机（下线）的。</span></p>
  <p data-lake-id="u9f52524f" id="u9f52524f"><span data-lake-id="u1de79164" id="u1de79164">​</span><br></p>
  <p data-lake-id="u0ccb583d" id="u0ccb583d"><span data-lake-id="ued2ad7c3" id="ued2ad7c3">对于服务提供方来说，应用下线时，先标记为不接受新请求，请求过来时直接报错，让客户端重试。然后，检测线程池中的线程是否正在运行，如果有，那就等线程执行完。</span></p>
  <p data-lake-id="u0ce1538e" id="u0ce1538e"><span data-lake-id="u6c278d89" id="u6c278d89">​</span><br></p>
  <p data-lake-id="u78ae1201" id="u78ae1201"><span data-lake-id="u6dc0446d" id="u6dc0446d">对于服务消费方来说，应用下线时，不再发起新的调用请求，所有的新调用在客户端侧直接报错。然后，检测还有没有远程请求没有得到返回的，如果有，则等待返回结果。</span></p>
  <p data-lake-id="u2c00a220" id="u2c00a220"><br></p>
  <h1 data-lake-id="ADeyf" id="ADeyf"><span data-lake-id="ude9e12a1" id="ude9e12a1">扩展知识</span></h1>
  <h2 data-lake-id="BuUSD" id="BuUSD"><br></h2>
  <h2 data-lake-id="IKSBR" id="IKSBR"><span data-lake-id="ufebf114e" id="ufebf114e">优雅上下线</span></h2>
  <p data-lake-id="uc243130d" id="uc243130d"><br></p>
  <p data-lake-id="u0c8612bc" id="u0c8612bc"><span data-lake-id="u37b14514" id="u37b14514">关于"优雅上下线"这个词，我没找到官方的解释，我尝试解释一下这是什么。</span></p>
  <p data-lake-id="u8191479a" id="u8191479a"><br></p>
  <p data-lake-id="uc018e683" id="uc018e683"><span data-lake-id="u67aa9d38" id="u67aa9d38">首先，上线、下线大家一定都很清楚，比如我们一次应用发布过程中，就需要先将应用服务停掉，然后再把服务启动起来。这个过程就包含了一次下线和一次上线。</span></p>
  <p data-lake-id="u064e3e59" id="u064e3e59"><br></p>
  <p data-lake-id="u5eb08693" id="u5eb08693"><span data-lake-id="ucd4117ab" id="ucd4117ab">那么，"优雅"怎么理解呢？</span></p>
  <p data-lake-id="u2e97c04c" id="u2e97c04c"><br></p>
  <p data-lake-id="u26d52ea5" id="u26d52ea5"><span data-lake-id="u093f4556" id="u093f4556">先说什么情况我们认为不优雅：</span></p>
  <p data-lake-id="u99be706e" id="u99be706e"><br></p>
  <p data-lake-id="u978b24d7" id="u978b24d7"><span data-lake-id="u48ae46bc" id="u48ae46bc">1、服务停止时，没有关闭对应的监控，导致应用停止后发生大量报警。</span></p>
  <p data-lake-id="ud9e1651b" id="ud9e1651b"><br></p>
  <p data-lake-id="u44ad3222" id="u44ad3222"><span data-lake-id="u5d59be28" id="u5d59be28">2、应用停止时，没有通知外部调用方，很多请求还会过来，导致很多调用失败。</span></p>
  <p data-lake-id="u614971bc" id="u614971bc"><br></p>
  <p data-lake-id="u6dca3d71" id="u6dca3d71"><span data-lake-id="ud154db5b" id="ud154db5b">3、应用停止时，有线程正在执行中，执行了一半，JVM进程就被干掉了。</span></p>
  <p data-lake-id="u2a2dd961" id="u2a2dd961"><br></p>
  <p data-lake-id="ua17a5170" id="ua17a5170"><span data-lake-id="uead9c838" id="uead9c838">4、应用启动时，服务还没准备好，就开始对外提供服务，导致很多失败调用。</span></p>
  <p data-lake-id="ue77ed337" id="ue77ed337"><br></p>
  <p data-lake-id="u15c28dc5" id="u15c28dc5"><span data-lake-id="u89b61deb" id="u89b61deb">5、应用启动时，没有检查应用的健康状态，就开始对外提供服务，导致很多失败调用。</span></p>
  <p data-lake-id="u6698bcca" id="u6698bcca"><br></p>
  <p data-lake-id="u78deda99" id="u78deda99"><span data-lake-id="u1ee84482" id="u1ee84482">以上，都是我们认为的不优雅的情况，那么，反过来，优雅上下线就是一种避免上述情况发生的手段。</span></p>
  <p data-lake-id="u49ea21c5" id="u49ea21c5"><br></p>
  <p data-lake-id="u48e8dd86" id="u48e8dd86"><span data-lake-id="ubd3903b0" id="ubd3903b0">一个应用的优雅上下线涉及到的内容其实有很多，从底层的操作系统、容器层面，到编程语言、框架层面，再到应用架构层面，涉及到的知识很广泛。</span></p>
  <p data-lake-id="uf7e89b3f" id="uf7e89b3f"><br></p>
  <p data-lake-id="u58860f04" id="u58860f04"><span data-lake-id="ub25299e7" id="ub25299e7">其实，优雅上下线中，最重要的还是优雅下线。因为如果下线过程不优雅的话，就会发生很多调用失败了、服务找不到等问题。所以很多时候，大家也会提优雅停机这样的概念。</span></p>
  <p data-lake-id="ud1238a52" id="ud1238a52"><br></p>
  <p data-lake-id="ue9bc95f3" id="ue9bc95f3"><span data-lake-id="u74db3d64" id="u74db3d64">本文后面介绍的优雅上下线也重点关注优雅停机的过程。</span></p>
  <p data-lake-id="u46d12dac" id="u46d12dac"><br></p>
  <h2 data-lake-id="nqmrD" id="nqmrD"><span data-lake-id="ue6b3b3a5" id="ue6b3b3a5">操作系统&amp;容器的优雅上下线</span></h2>
  <p data-lake-id="uef802016" id="uef802016"><br></p>
  <p data-lake-id="ub7fd0f65" id="ub7fd0f65"><span data-lake-id="u78f5917c" id="u78f5917c">我们知道，</span><code data-lake-id="u218830cc" id="u218830cc"><span data-lake-id="u939790f0" id="u939790f0">kill -9</span></code><span data-lake-id="u8552213d" id="u8552213d">之所以不建议使用，是因为</span><code data-lake-id="u60f4c56e" id="u60f4c56e"><span data-lake-id="u50b0e867" id="u50b0e867">kill -9</span></code><span data-lake-id="uf3df81c6" id="uf3df81c6">特别强硬，系统会发出SIGKILL信号，他要求接收到该信号的程序应该立即结束运行，不能被阻塞或者忽略。</span></p>
  <p data-lake-id="u9864c480" id="u9864c480"><br></p>
  <p data-lake-id="u988c53bf" id="u988c53bf"><span data-lake-id="u0ab806e2" id="u0ab806e2">这个过程显然是不优雅的，因为应用立刻停止的话，就没办法做收尾动作。而更优雅的方式是</span><code data-lake-id="ue438e281" id="ue438e281"><span data-lake-id="u6e9f5a0f" id="u6e9f5a0f">kill -15</span></code><span data-lake-id="u65bc3981" id="u65bc3981">。</span></p>
  <p data-lake-id="u8497ee8e" id="u8497ee8e"><br></p>
  <p data-lake-id="u1c0fd4d0" id="u1c0fd4d0"><span data-lake-id="ufdf3a086" id="ufdf3a086">当使用</span><code data-lake-id="ub2f3a63a" id="ub2f3a63a"><span data-lake-id="ud46598f4" id="ud46598f4">kill -15</span></code><span data-lake-id="u345c79c5" id="u345c79c5">时，系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后，具体要如何处理是自己可以决定的。</span></p>
  <p data-lake-id="u3d31b1fe" id="u3d31b1fe"><br></p>
  <p data-lake-id="u0818f02a" id="u0818f02a"><code data-lake-id="u9dcb5499" id="u9dcb5499"><span data-lake-id="u8f2513d6" id="u8f2513d6">kill -15</span></code><span data-lake-id="u57e86e90" id="u57e86e90">会通知到应用程序，这就是操作系统对于优雅上下线的最基本的支持。</span></p>
  <p data-lake-id="u42ecd27d" id="u42ecd27d"><br></p>
  <p data-lake-id="u49235d60" id="u49235d60"><span data-lake-id="u142ed790" id="u142ed790">以前，在操作系统之上就是应用程序了，但是，自从容器化技术推出之后，在操作系统和应用程序之间，多了一个容器层，而Docker、k8s等容器其实也是支持优雅上下线的。</span></p>
  <p data-lake-id="u6082990f" id="u6082990f"><br></p>
  <p data-lake-id="u982a3f59" id="u982a3f59"><span data-lake-id="u24d00b23" id="u24d00b23">如Docker中同样提供了两个命令， </span><code data-lake-id="uc4511bc2" id="uc4511bc2"><span data-lake-id="u7e0a6712" id="u7e0a6712">docker stop</span></code><span data-lake-id="ue503ea49" id="ue503ea49"> 和 </span><code data-lake-id="u48848ba6" id="u48848ba6"><span data-lake-id="u031e1249" id="u031e1249">docker kill</span></code></p>
  <p data-lake-id="u5887883e" id="u5887883e"><br></p>
  <p data-lake-id="ua542bcfb" id="ua542bcfb"><code data-lake-id="u193edb78" id="u193edb78"><span data-lake-id="u27a35f49" id="u27a35f49">docker stop</span></code><span data-lake-id="u742c26b1" id="u742c26b1">就像</span><code data-lake-id="u3ee3ed38" id="u3ee3ed38"><span data-lake-id="u0c6b9fe1" id="u0c6b9fe1">kill -15</span></code><span data-lake-id="ud549f87a" id="ud549f87a">一样，他会向容器内的进程发送SIGTERM信号，在10S之后（可通过参数指定）再发送SIGKILL信号。</span></p>
  <p data-lake-id="ua6ac06a6" id="ua6ac06a6"><br></p>
  <p data-lake-id="u9177e354" id="u9177e354"><span data-lake-id="u2f673976" id="u2f673976">而</span><code data-lake-id="ueb617ba1" id="ueb617ba1"><span data-lake-id="u44984a80" id="u44984a80">docker kill</span></code><span data-lake-id="u2df9b872" id="u2df9b872">就像</span><code data-lake-id="ufe15cabd" id="ufe15cabd"><span data-lake-id="u38509c67" id="u38509c67">kill -9</span></code><span data-lake-id="ue40af79a" id="ue40af79a">，直接发送SIGKILL信号。</span></p>
  <p data-lake-id="u42afab03" id="u42afab03"><br></p>
  <h2 data-lake-id="Ze8oj" id="Ze8oj"><span data-lake-id="u129befe7" id="u129befe7">JVM的优雅上下线</span></h2>
  <p data-lake-id="u62226159" id="u62226159"><br></p>
  <p data-lake-id="u94077f07" id="u94077f07"><span data-lake-id="u7a4b8e3a" id="u7a4b8e3a">在操作系统、容器等对优雅上下线有了基本的支持之后，在接收到</span><code data-lake-id="ua8710467" id="ua8710467"><span data-lake-id="u1fb6d5b9" id="u1fb6d5b9">docker stop</span></code><span data-lake-id="u70b14569" id="u70b14569">、</span><code data-lake-id="u81485611" id="u81485611"><span data-lake-id="uab96a455" id="uab96a455">kill -15</span></code><span data-lake-id="u6b1dfa6e" id="u6b1dfa6e">等命令后，会通知应用进程进行进程关闭。</span></p>
  <p data-lake-id="ub91cd58f" id="ub91cd58f"><br></p>
  <p data-lake-id="u12dafc0a" id="u12dafc0a"><span data-lake-id="ue4ce80fe" id="ue4ce80fe">而Java应用在运行时就是一个独立运行的进程，这个进程是如何关闭的呢？</span></p>
  <p data-lake-id="uf071ec04" id="uf071ec04"><br></p>
  <p data-lake-id="ueb63d3b5" id="ueb63d3b5"><span data-lake-id="ub445c88e" id="ub445c88e">Java程序的终止运行是基于JVM的关闭实现的，JVM关闭方式分为正常关闭、强制关闭和异常关闭3种。</span></p>
  <p data-lake-id="u96615907" id="u96615907"><br></p>
  <p data-lake-id="ueaee578a" id="ueaee578a"><span data-lake-id="u84e7a75a" id="u84e7a75a">这其中，正常关闭就是支持优雅上下线的。正常关闭过程中，JVM可以做一些清理动作，比如删除临时文件。</span></p>
  <p data-lake-id="ue70f8cab" id="ue70f8cab"><br></p>
  <p data-lake-id="u5e88e86a" id="u5e88e86a"><span data-lake-id="u1f8fe698" id="u1f8fe698">当然，开发者也是可以自定义做一些额外的事情的，比如通知应用框架优雅上下线操作。</span></p>
  <p data-lake-id="u2141c02e" id="u2141c02e"><br></p>
  <p data-lake-id="ua1253ca6" id="ua1253ca6"><span data-lake-id="ue3c8598a" id="ue3c8598a">而这种机制</span><strong><span data-lake-id="u543fdee7" id="u543fdee7">是通过JDK中提供的shutdown hook实现的。JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法，可以注册一个JVM关闭的钩子。</span></strong></p>
  <p data-lake-id="udb779726" id="udb779726"><br></p>
  <p data-lake-id="u5fe0ded5" id="u5fe0ded5"><span data-lake-id="u7b7e47a5" id="u7b7e47a5">例子如下：</span></p>
  <p data-lake-id="uf6407429" id="uf6407429"><br></p>
  <pre lang="java"><code>
package com.hollis;

public class ShutdownHookTest {

    public static void main(String[] args) {
        boolean flag = true;
        Runtime.getRuntime().addShutdownHook(new Thread(() -&gt; {
            System.out.println("hook execute...");
        }));

        while (flag) {
            // app is runing
        }

        System.out.println("main thread execute end...");
    }
}
</code></pre>
  <p data-lake-id="u4f514cae" id="u4f514cae"><br></p>
  <p data-lake-id="ucc465898" id="ucc465898"><span data-lake-id="u83730e34" id="u83730e34">执行命令：</span></p>
  <p data-lake-id="u526308c4" id="u526308c4"><br></p>
  <pre lang="java"><code>
➜ jps
6520 ShutdownHookTest
6521 Jps
➜ kill 6520
</code></pre>
  <p data-lake-id="ud5b0fafb" id="ud5b0fafb"><br></p>
  <p data-lake-id="uaebab297" id="uaebab297"><span data-lake-id="u61c880e4" id="u61c880e4">控制台输出内容：</span></p>
  <p data-lake-id="u55a9592b" id="u55a9592b"><br></p>
  <pre lang="java"><code>
hook execute...
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)
</code></pre>
  <p data-lake-id="u04ef585f" id="u04ef585f"><br></p>
  <p data-lake-id="u418352eb" id="u418352eb"><span data-lake-id="u56e2bac3" id="u56e2bac3">可以看到，当我们使用kill（默认kill -15）关闭进程的时候，程序会先执行我注册的shutdownHook，然后再退出，并且会给出一个提示：</span><code data-lake-id="ub34cb18a" id="ub34cb18a"><span data-lake-id="ud761e585" id="ud761e585">interrupted by signal 15: SIGTERM</span></code></p>
  <p data-lake-id="u38119272" id="u38119272"><br></p>
  <h2 data-lake-id="R2rEz" id="R2rEz"><span data-lake-id="u2150c074" id="u2150c074">Spring的优雅上下线</span></h2>
  <p data-lake-id="ua89625b9" id="ua89625b9"><br></p>
  <p data-lake-id="u6416a4dc" id="u6416a4dc"><span data-lake-id="u86a85e86" id="u86a85e86">有了JVM提供的shutdown hook之后，很多框架都可以通过这个机制来做优雅下线的支持。</span></p>
  <p data-lake-id="u9bcf31cc" id="u9bcf31cc"><br></p>
  <p data-lake-id="ue3ade385" id="ue3ade385"><span data-lake-id="ud1f7e110" id="ud1f7e110">比如Spring，他就会向JVM注册一个shutdown hook，在接收到关闭通知的时候，进行bean的销毁，容器的销毁处理等操作。</span></p>
  <p data-lake-id="u08c10e2a" id="u08c10e2a"><br></p>
  <p data-lake-id="u86b819e2" id="u86b819e2"><span data-lake-id="u74c8b17b" id="u74c8b17b">同时，作为一个成熟的框架，Spring也提供了事件机制，可以借助这个机制实现更多的优雅上下线功能。</span></p>
  <p data-lake-id="ud674b700" id="ud674b700"><br></p>
  <p data-lake-id="u5d1bc77b" id="u5d1bc77b"><span data-lake-id="u1b3fc4cf" id="u1b3fc4cf">ApplicationListener是Spring事件机制的一部分，与抽象类ApplicationEvent类配合来完成ApplicationContext的事件机制。</span></p>
  <p data-lake-id="u9396c454" id="u9396c454"><br></p>
  <p data-lake-id="u8d17f498" id="u8d17f498"><span data-lake-id="u54daf33f" id="u54daf33f">开发者可以实现ApplicationListener接口，监听到 Spring 容器的关闭事件（ContextClosedEvent），来做一些特殊的处理：</span></p>
  <p data-lake-id="u53be961a" id="u53be961a"><br></p>
  <pre lang="java"><code>
@Component
public class MyListener implements ApplicationListener&lt;ContextClosedEvent&gt; {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 做容器关闭之前的清理工作
    }

}
</code></pre>
  <p data-lake-id="ud1b5e05f" id="ud1b5e05f"><br></p>
  <h2 data-lake-id="mD3VP" id="mD3VP"><span data-lake-id="u512a64db" id="u512a64db">Dubbo的优雅上下线</span></h2>
  <p data-lake-id="u44e114f8" id="u44e114f8"><br></p>
  <p data-lake-id="u5c1a211d" id="u5c1a211d"><span data-lake-id="u36378459" id="u36378459">因为Spring中提供了ApplicationListener接口，帮助我们来监听容器关闭事件，那么，很多web容器、框架等就可以借助这个机制来做自己的优雅上下线操作。</span></p>
  <p data-lake-id="u2d526085" id="u2d526085"><br></p>
  <p data-lake-id="u520334eb" id="u520334eb"><span data-lake-id="uf33e9e34" id="uf33e9e34">如tomcat、dubbo等都是这么做的。</span></p>
  <p data-lake-id="u46460480" id="u46460480"><br></p>
  <p data-lake-id="u576f9a45" id="u576f9a45"><span data-lake-id="ud4076337" id="ud4076337">应用在停机时，接收到关闭通知时，会先把自己标记为不接受（发起）新请求，然后再等待10s（默认是10秒）的时候，等执行中的线程执行完。</span></p>
  <p data-lake-id="u5ca4041f" id="u5ca4041f"><br></p>
  <p data-lake-id="ubc8da2c1" id="ubc8da2c1"><span data-lake-id="ud4de6d2b" id="ud4de6d2b">那么，之所以他能做这些事，是因为从操作系统、到JVM、到Spring等都对优雅停机做了很好的支持。</span></p>
  <p data-lake-id="ubd6503d3" id="ubd6503d3"><br></p>
  <p data-lake-id="u1efd178b" id="u1efd178b"><span data-lake-id="ua119ccbe" id="ua119ccbe">关于Dubbo各个版本中具体是如何借助JVM的shutdown hook机制、或者说Spring的事件机制做的优雅停机，我的一位同事的一篇文章介绍的很清晰，大家可以看下：</span></p>
  <p data-lake-id="uc62149ee" id="uc62149ee"><br></p>
  <p data-lake-id="uf58a6e67" id="uf58a6e67"><a href="https://www.cnkirito.moe/dubbo-gracefully-shutdown/" target="_blank" data-lake-id="ub35dea91" id="ub35dea91"><span data-lake-id="u1cbdd3a6" id="u1cbdd3a6">https://www.cnkirito.moe/dubbo-gracefully-shutdown/</span></a></p>
  <p data-lake-id="u08b240b0" id="u08b240b0"><br></p>
  <p data-lake-id="u1a0f3dc6" id="u1a0f3dc6"><span data-lake-id="u3b299a69" id="u3b299a69">在从Dubbo 2.5 到 Dubbo 2.7介绍了历史版本中，Dubbo为了解决优雅上下线问题所遇到的问题和方案。</span></p>
  <p data-lake-id="u1c543ff6" id="u1c543ff6"><br></p>
  <p data-lake-id="u7955d8f4" id="u7955d8f4"><span data-lake-id="ubebb6f92" id="ubebb6f92">目前，Dubbo中实现方式如下，同样是用到了Spring的事件机制：</span></p>
  <p data-lake-id="uf2f0a346" id="uf2f0a346"><br></p>
  <pre lang="java"><code>
public class SpringExtensionFactory implements ExtensionFactory {
    public static void addApplicationContext(ApplicationContext context) {
        CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
        BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);
    }
}
</code></pre>
  <p data-lake-id="u7c478517" id="u7c478517"><br></p>
 </body>
</html>